/*
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.sockets.applets.java;

import netscape.javascript.JSObject;

import java.applet.Applet;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * The applet which provides socket functionality. To work correctly this applet must be signed.
 * When JS makes calls to this applet (for connecting), JVM checks if JS is signed and
 * grants corresponding permissions for thread from JS. Because JS is not always signed (so JS thread won't have
 * all needed permissions), JS thread will be used only to notify thread of applet. Notified thread will do all work:
 * connect, etc.
 *
 * @author Aleksey Lagoshin
 */
public class TextApplet extends Applet {

  private static final String E_INPUT_BUFF_NULL = "The inputBuffer is null, you need to connect to server first";

  /**
   * Action to do nothing.
   */
  private static final int ACT_NOTHING = -1;
  /**
   * Action to connect.
   */
  private static final int ACT_CONNECT = 0;
  /**
   * Action to shutdown this applet.
   */
  private static final int ACT_SHUTDOWN = 1;

  /**
   * What should this applet do after notifying.
   */
  private int currentAction = ACT_NOTHING;

  /**
   * Applet identifier.
   */
  private String id;

  /**
   * Remote host to connect.
   */
  private String host;
  /**
   * Port to connect.
   */
  private int port;

  /**
   * JavaScript Window object.
   */
  private JSObject window;

  /**
   * The socket object.
   */
  private Socket socket;
  /**
   * Input stream.
   */
  private InputStreamReader in;
  /**
   * Output stream.
   */
  private OutputStream out;
  /**
   * The input buffer for received data, JavaScript can take this data using read method.
   */
  private List inputBuffer;

  /**
   * This thread is used for reading bytes from input stream.
   */
  private Thread readingThread;

  /**
   * Indicates whether socket has been closed manually or not
   */
  private boolean manuallyClosed = false;

  // Names of JavaScript functions
  private String funcOnReady;
  private String funcOnConnect;
  private String funcOnCloseConnect;
  private String funcOnData;
  private String funcOnIOError;
  private String funcOnSecurityError;

  /**
   * Starts an additional thread that will wait for commands from JavaScript.
   */
  public void start() {
    System.out.println("Text Sockets Applet is started");

    id = getParameter("id");

    // Initializing names of JavaScript functions using the specified ID
    funcOnReady = "java_" + id + "_onReady";
    funcOnConnect = "java_" + id + "_onConnect";
    funcOnCloseConnect = "java_" + id + "_onClose";
    funcOnData = "java_" + id + "_onData";
    funcOnIOError = "java_" + id + "_onIOError";
    funcOnSecurityError = "java_" + id + "_onSecurityError";

    // We need to free main thread, because some browsers won't work correctly
    new Thread(new Runnable() {
      public void run() {
        do {
          // Waiting for commands from JS
          synchronized (TextApplet.this) {
            currentAction = ACT_NOTHING;
            try {
              TextApplet.this.wait();
            }
            catch (InterruptedException e) {
              System.out.println("Text Sockets Applet has been terminated by browser");
              currentAction = ACT_SHUTDOWN;
            }

            dispatchCommand();
          }
        } while (currentAction != ACT_SHUTDOWN);
        System.out.println("Text Sockets Applet is finished");
      }
    }, "TextSocketsThread").start();

    // Tells the JavaScript that this applet is ready now
    window = JSObject.getWindow(this);
    window.call(funcOnReady, new Object[]{this});
  }

  /**
   * Dispatching commands from JS.
   */
  private void dispatchCommand() {
    switch (currentAction) {
      case(ACT_CONNECT):
        connect();
        break;
    }
  }

  /**
   * Connects to host:port and reads bytes from input stream in separate thread.
   */
  private void connect() {
    // Closing previous connection
    if (readingThread != null)
      close();
    inputBuffer = new ArrayList();
    readingThread = new Thread(new Runnable() {
      public void run() {
        System.out.println("Connecting to " + host + ':' + port);
        try {
          socket = new Socket(host, port);
          in = new InputStreamReader(socket.getInputStream(), "UTF-8");
          out = socket.getOutputStream();

          System.out.println("Connected");
          window.call(funcOnConnect, null);
          // Start reading bytes from input stream
          startReading();
        }
        catch (SecurityException e) {
          System.out.println("Applet hasn't got all needed permissions:");
          e.printStackTrace();
          window.call(funcOnSecurityError, new Object[] {e.getMessage()});
        }
        catch (IOException e) {
          System.out.println("Couldn't connect. Exception:");
          e.printStackTrace();
          window.call(funcOnIOError, new Object[] {e.getMessage()});
        }
      }
    }, "TextReadingThread");
    readingThread.start();
  }

  /**
   * Reads bytes from input stream, appends them to a string buffer and adds that buffer to inputBuffer
   * if zero byte is received.
   */
  private void startReading() {
    System.out.println("Reading bytes from input stream...");

    StringBuffer buffer = new StringBuffer();
    int b;

    try {
      while (!readingThread.isInterrupted()) {
        // Reading byte
        b = in.read();
        // If readed byte is zero, then we need to store received string in inputBuffer
        if (b == 0 && buffer.length() > 0) {
          String receivedString = buffer.toString();
          inputBuffer.add(receivedString);
          window.call(funcOnData, new Object[]{Integer.valueOf(receivedString.length())});
          buffer.setLength(0);
        }
        // End of stream
        else if (b == -1) {
          close();
          window.call(funcOnCloseConnect, null);
          break;
        }
        else
          // Appending received character to string buffer
          buffer.append(new Character((char) b));
      }
    }
    catch (IOException e) {
      if (!manuallyClosed) {
        System.out.println("Exception while reading bytes:");
        e.printStackTrace();
        close();
        window.call(funcOnCloseConnect, null);
      }  
    }
  }

  // Interface for JavaScript

  /**
   * Interface method for JavaScript, tells this applet to connect to host:port.
   *
   * @param host the host name
   * @param port the port number
   */
  public void connect(String host, int port) {
    synchronized (this) {
      currentAction = ACT_CONNECT;
      manuallyClosed = false;
      this.host = host;
      this.port = port;
      notify();
    }
  }

  /**
   * Interface method for JavaScript, tells this applet to close connection.
   *
   * @return an exception if an error occurs when closing that connection
   */
  public String close() {
    System.out.println("Closing connection");

    try {
      if (socket != null) {
        manuallyClosed = true;
        if (socket.getInputStream() != null)
          socket.getInputStream().close();
        if (out != null)
          out.close();

        socket.close();
      }
      return null;
    }
    catch (IOException e) {
      System.out.println("Exception when closing connection:");
      e.printStackTrace();
      return "E1" + e.getMessage();
    }
  }

  /**
   * Shutting down the main thread, after this operation the applet won't be able to connect to servers.
   */
  public void shutdown() {
    synchronized (this) {
      close();
      currentAction = ACT_SHUTDOWN;
      notify();
    }
  }

  /**
   * Interface method for JavaScript, tells this applet to send the message to server.
   *
   * @param message the message
   * @return an exceptions if an error occurs
   */
  public String write(String message) {
    try {
      out.write(message.getBytes("UTF-8"));
      out.write(0);
      return null;
    }
    catch (IOException e) {
      System.out.println("Exception while sending data:");
      e.printStackTrace();
      return "E1" + e.getMessage();
    }
  }

  /**
   * Interface method for JavaScript, returns received string.
   *
   * @return a received string or an exception
   */
  public String read() {
    if (inputBuffer == null)
      return "E1" + E_INPUT_BUFF_NULL;
    else if (inputBuffer.size() == 0)
      return "E2" + "There are no strings terminated by zero in the input buffer";
    else {
      String result = (String) inputBuffer.get(0);
      inputBuffer.remove(0);
      return "0" + result;
    }
  }

  /**
   * Interface method for JavaScript, returns the amount of strings terminated by zero byte in the input buffer.
   *
   * @return the amount of strings or exception if inputBuffer is null
   */
  public Object available() {
    if (inputBuffer == null)
      return "E1" + E_INPUT_BUFF_NULL;
    return Integer.valueOf(inputBuffer.size());
  }

}
